import 'dart:async';

import 'package:rxdart/rxdart.dart';

import '../../common/test_page.dart';
import '../utils.dart';

List<Stream<int>> _createTestStreams() {
  const intervals = [22, 50, 30, 40, 60];
  final ticker =
  Stream<int>.periodic(const Duration(milliseconds: 1), (index) => index)
      .skip(1)
      .take(300)
      .asBroadcastStream();

  return [
    ticker
        .where((index) => index % intervals[0] == 0)
        .map((index) => index ~/ intervals[0] - 1),
    ticker
        .where((index) => index % intervals[1] == 0)
        .map((index) => index ~/ intervals[1] - 1),
    ticker
        .where((index) => index % intervals[2] == 0)
        .map((index) => index ~/ intervals[2] - 1),
    ticker
        .where((index) => index % intervals[3] == 0)
        .map((index) => index ~/ intervals[3] - 1),
    ticker
        .where((index) => index % intervals[4] == 0)
        .map((index) => index ~/ intervals[4] - 1)
  ];
}

class WithLatestFromTestPage extends TestPage {
  WithLatestFromTestPage(super.title) {
    test('Rx.withLatestFrom', () async {
      final streams = _createTestStreams();

      expect(
          streams.first
              .withLatestFrom(
              streams[1], (first, int second) => Pair(first, second))
              .take(5));
    });

    test('Rx.withLatestFrom.iterate.once', () async {
      var iterationCount = 0;

      final combined = Stream.value(1).withLatestFromList(() sync* {
        ++iterationCount;
        yield Stream.value(2);
        yield Stream.value(3);
      }());

      expect(combined);
      expect(iterationCount);
    });

    test('Rx.withLatestFrom.reusable', () async {
      final streams = _createTestStreams();
      final transformer = WithLatestFromStreamTransformer.with1<int, int, Pair>(
          streams[1], (first, second) => Pair(first, second));
      const expectedOutput = [
        Pair(2, 0),
        Pair(3, 0),
        Pair(4, 1),
        Pair(5, 1),
        Pair(6, 2)
      ];
      var countA = 0, countB = 0;

      streams.first.transform(transformer).take(5).listen(((result) {
        expect(result);
      }));

      streams.first.transform(transformer).take(5).listen(((result) {
        expect(result);
      }));
    });

    test('Rx.withLatestFrom.asBroadcastStream', () async {
      final streams = _createTestStreams();
      final stream = streams.first.withLatestFrom(streams[1], (first, int second) => 0);

      stream.listen(null);
      stream.listen(null);

      expect(true);
    });

    test('Rx.withLatestFrom.error.shouldThrowA', () async {
      final streams = _createTestStreams();
      final streamWithError = Stream<int>.error(Exception())
          .withLatestFrom(streams[1], (first, int second) => 'Hello');

      streamWithError.listen(null,
          onError: (Object e, StackTrace s) {
            expect(e);
          });
    });

    test('Rx.withLatestFrom.error.shouldThrowB', () async {
      final streams = _createTestStreams();
      final stream = streams[1].take(1).withLatestFrom(
          Stream<int>.value(0), (first, int second) => throw Exception());

      expect(stream);
    });

    test('Rx.withLatestFrom.pause.resume', () async {
      late StreamSubscription<Pair> subscription;
      const expectedOutput = [Pair(2, 0)];
      final streams = _createTestStreams();
      var count = 0;

      subscription = streams.first
          .withLatestFrom(streams[1], (first, int second) => Pair(first, second))
          .take(1)
          .listen(((result) {
        expect(result);

        if (count == expectedOutput.length) {
          subscription.cancel();
        }
      }));

      subscription.pause();
      subscription.resume();
    });

    test('Rx.withLatestFrom.otherEmitsNull', () async {
      const expected = Pair(1, null);
      final stream = Rx.timer(
        1,
        const Duration(microseconds: 100),
      ).withLatestFrom(
        Stream<int?>.value(null),
            (a, int? b) => Pair(a, b),
      );

      expect(stream);
    });

    test('Rx.withLatestFrom.otherNotEmit', () async {
      final stream = Rx.timer(
        1,
        const Duration(microseconds: 100),
      ).withLatestFrom(
        Stream<int>.empty(),
            (a, int b) => Pair(a, b),
      );

      expect(stream);
    });

    test('Rx.withLatestFrom2', () async {
      final streams = _createTestStreams();

      streams.first
          .withLatestFrom2(
        streams[1],
        streams[2],
            (item1, int item2, int item3) => _Tuple(item1, item2, item3),
      )
          .take(5)
          .listen((result) => expect(result),
      );
    });

    test('Rx.withLatestFrom3', () async {
      const expectedOutput = [
        _Tuple(2, 0, 1, 0),
        _Tuple(3, 0, 1, 1),
        _Tuple(4, 1, 2, 1),
        _Tuple(5, 1, 3, 2),
        _Tuple(6, 2, 4, 2),
      ];
      final streams = _createTestStreams();
      var count = 0;

      streams.first
          .withLatestFrom3(
        streams[1],
        streams[2],
        streams[3],
            (item1, int item2, int item3, int item4) =>
            _Tuple(item1, item2, item3, item4),
      )
          .take(5)
          .listen((result) => expect(result),
      );
    });

    test('Rx.withLatestFrom4', () async {
      final streams = _createTestStreams();

      streams.first
          .withLatestFrom4(
        streams[1],
        streams[2],
        streams[3],
        streams[4],
            (item1, int item2, int item3, int item4, int item5) =>
            _Tuple(item1, item2, item3, item4, item5),
      )
          .take(5)
          .listen((result) => expect(result),
      );
    });

    test('Rx.withLatestFrom5', () async {
      final stream = Rx.timer(
        1,
        const Duration(microseconds: 100),
      ).withLatestFrom5(
        Stream.value(2),
        Stream.value(3),
        Stream.value(4),
        Stream.value(5),
        Stream.value(6),
            (a, int b, int c, int d, int e, int f) => _Tuple(a, b, c, d, e, f),
      );

      expect(stream);
    });

    test('Rx.withLatestFrom6', () async {
      final stream = Rx.timer(
        1,
        const Duration(microseconds: 100),
      ).withLatestFrom6(
        Stream.value(2),
        Stream.value(3),
        Stream.value(4),
        Stream.value(5),
        Stream.value(6),
        Stream.value(7),
            (a, int b, int c, int d, int e, int f, int g) =>
            _Tuple(a, b, c, d, e, f, g),
      );

      expect(stream);
    });

    test('Rx.withLatestFrom7', () async {
      final stream = Rx.timer(
        1,
        const Duration(microseconds: 100),
      ).withLatestFrom7(
        Stream.value(2),
        Stream.value(3),
        Stream.value(4),
        Stream.value(5),
        Stream.value(6),
        Stream.value(7),
        Stream.value(8),
            (a, int b, int c, int d, int e, int f, int g, int h) =>
            _Tuple(a, b, c, d, e, f, g, h),
      );

      expect(stream);
    });

    test('Rx.withLatestFrom8', () async {
      final stream = Rx.timer(
        1,
        const Duration(microseconds: 100),
      ).withLatestFrom8(
        Stream.value(2),
        Stream.value(3),
        Stream.value(4),
        Stream.value(5),
        Stream.value(6),
        Stream.value(7),
        Stream.value(8),
        Stream.value(9),
            (a, int b, int c, int d, int e, int f, int g, int h, int i) =>
            _Tuple(a, b, c, d, e, f, g, h, i),
      );

      expect(stream);
    });

    test('Rx.withLatestFrom9', () async {
      final stream = Rx.timer(
        1,
        const Duration(microseconds: 100),
      ).withLatestFrom9(
        Stream.value(2),
        Stream.value(3),
        Stream.value(4),
        Stream.value(5),
        Stream.value(6),
        Stream.value(7),
        Stream.value(8),
        Stream.value(9),
        Stream.value(10),
            (a, int b, int c, int d, int e, int f, int g, int h, int i, int j) =>
            _Tuple(a, b, c, d, e, f, g, h, i, j),
      );

      expect(stream);
    });

    test('Rx.withLatestFromList', () async {
      final stream = Rx.timer(
        1,
        const Duration(microseconds: 100),
      ).withLatestFromList(
        [
          Stream.value(2),
          Stream.value(3),
          Stream.value(4),
          Stream.value(5),
          Stream.value(6),
          Stream.value(7),
          Stream.value(8),
          Stream.value(9),
          Stream.value(10),
        ],
      );
      const expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

      expect(stream);
    });

    test('Rx.withLatestFromList.emptyList', () async {
      final stream = Stream.fromIterable([1, 2, 3]).withLatestFromList([]);

      expect(stream);
    });
    test('Rx.withLatestFrom accidental broadcast', () async {
      final controller = StreamController<int>();

      final stream = controller.stream
          .withLatestFrom(Stream<int>.empty(), (_, dynamic __) => true);

      stream.listen(null);
      expect(() => stream.listen(null));

      controller.add(1);
    });

    test('Rx.withLatestFrom.nullable', () {
      nullableTest<List<String?>>(
            (s) => s.withLatestFromList([Stream.value('String')]),
      );
    });
  }

}

class Pair {
  final int? first;
  final int? second;

  const Pair(this.first, this.second);

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    return other is Pair && first == other.first && second == other.second;
  }

  @override
  int get hashCode {
    return first.hashCode ^ second.hashCode;
  }

  @override
  String toString() {
    return 'Pair{first: $first, second: $second}';
  }
}

class _Tuple {
  final int? item1;
  final int? item2;
  final int? item3;
  final int? item4;
  final int? item5;
  final int? item6;
  final int? item7;
  final int? item8;
  final int? item9;
  final int? item10;

  const _Tuple([
    this.item1,
    this.item2,
    this.item3,
    this.item4,
    this.item5,
    this.item6,
    this.item7,
    this.item8,
    this.item9,
    this.item10,
  ]);

  @override
  bool operator ==(dynamic other) {
    return identical(this, other) ||
        other is _Tuple &&
            item1 == other.item1 &&
            item2 == other.item2 &&
            item3 == other.item3 &&
            item4 == other.item4 &&
            item5 == other.item5 &&
            item6 == other.item6 &&
            item7 == other.item7 &&
            item8 == other.item8 &&
            item9 == other.item9 &&
            item10 == other.item10;
  }

  @override
  int get hashCode {
    return item1.hashCode ^
    item2.hashCode ^
    item3.hashCode ^
    item4.hashCode ^
    item5.hashCode ^
    item6.hashCode ^
    item7.hashCode ^
    item8.hashCode ^
    item9.hashCode ^
    item10.hashCode;
  }

  @override
  String toString() {
    final values = [
      item1,
      item2,
      item3,
      item4,
      item5,
      item6,
      item7,
      item8,
      item9,
      item10,
    ];
    final s = values.join(', ');
    return 'Tuple { $s }';
  }
}